using CodeAngel.Domain;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;

namespace CodeAngel.Domain.Framework
{

    #region Aggregate

    /// <summary>
    /// Base class for an aggregate.
    /// </summary>
    /// <remarks>
    /// An aggregate is an object that provides loading and saving a hierarchical graph of 
    /// related objects.  An aggregate knows how to:
    ///		* Get the data it needs for its object graph
    ///		* Construct its object graph
    ///		* Save changes in a transactional manner, including updates, inserts and deletes
    ///		* Ensures that the identified user has the respective permissions to create, read, update or delete
    /// </remarks>
    public abstract class Aggregate : INotifyPropertyChanged
    {
        #region Root

        /// <summary>
        /// Identifies the root entity of the object graph.
        /// </summary>
        public IAggregateRoot Root
        {
            get
            {
                return this._Root;
            }
            protected set
            {
                if (value != this._Root)
                {
                    this._Root = value;
                    this.RaisePropertyChanged("Root");
                }
            }
        }
        private IAggregateRoot _Root;

        #endregion

        #region User

        /// <summary>
        /// Gets or sets the user.
        /// </summary>
        public User User { get; set; }

        #endregion

        #region DtoHost

        public DtoHost DtoHost { get; set; }

        #endregion

        #region LoadAsync

        /// <summary>
        /// Instructs this aggregate to asynchronously load its object graph based on the identified 
        /// filter.
        /// </summary>
        public Task LoadAsync(Filter filter, User user)
        {
            this.User = user;
            if (this.User == null) throw new MissingUserException();
            this.ValidateFilter(filter);

            return Task.Factory.StartNew(() =>
            {
                var ThisServiceProxy = DomainDataServiceClient.Create();
                this.DtoHost = ThisServiceProxy.GetDtoHost(this.GetType().FullName, filter.ToSqlString(), this.User.UserName, this.User.Password);
                this.BuildObjectGraph();
                this.RaiseBuildObjectGraphCompletedEvent();
            });
        }

        #endregion

        #region ValidateFilter

        /// <summary>
        /// Validates that the identified filter is suitable for this aggregate and its LoadAsync operation.
        /// If it is, nothing will happen; otherwise the appropariate exception will be thrown.
        /// </summary>
        private void ValidateFilter(IFilter thisFilter)
        {
            if (thisFilter == null) throw new FilterIsNullException(); ;
            if (!this.IsFilterTypeValid(thisFilter)) throw new InvalidFilterException(this.GetType().FullName, thisFilter.GetType().FullName);
        }

        /// <summary>
        /// Asks the derived class if the identified filter object is valid for this aggregate.
        /// </summary>
        protected abstract bool IsFilterTypeValid(IFilter thisFilter);

        #endregion

        #region BuildObjectGraph

        /// <summary>
        /// Instructs the derived class to build its object graph (using the data from the DtoHost property)
        /// </summary>
        protected abstract void BuildObjectGraph();

        /// <summary>
        /// Internal build method to support the DeleteAggregate's need to generate a UserLogInAggregate...
        /// </summary>
        //internal void BuildObjectGraph(DtoHost dtoHost)
        [EditorBrowsable(EditorBrowsableState.Never)]
        public void BuildObjectGraph(DtoHost dtoHost)
        {
            this.DtoHost = dtoHost;
            this.BuildObjectGraph();
        }

        #region BuildObjectGraphCompleted

        /// <summary>
        /// Provides notification that the BuildObjectGraph has Completed.
        /// </summary>
        public event EventHandler BuildObjectGraphCompleted;

        /// <summary>
        /// Raises the BuildObjectGraphCompleted event.
        /// </summary>
        protected void RaiseBuildObjectGraphCompletedEvent()
        {
            if (this.BuildObjectGraphCompleted != null) this.BuildObjectGraphCompleted(this, EventArgs.Empty);
        }

        #endregion


        #endregion

        #region CanSave
        
        /// <summary>
        /// Identifies if the object graph managed by this aggregate is in a state where it can be saved.
        /// </summary>
        /// <remarks>
        /// All updateable entities in the object graph managed by this aggregate must be in a valid 
        /// state (IsValid = true) and the current User must have permission to save all updateable entities.
        /// </remarks>
        public bool CanSave()
        {
            return this.Root.IsValid() && this.UserHasPermissionToSave();
        }

        /// <summary>
        /// Asks the derived aggregate to determine if the current user has permission to save the 
        /// updateable entities in the object graph maintained by this aggregate.
        /// </summary>
        protected abstract bool UserHasPermissionToSave();

        /// <summary>
        /// Throws an exception if this aggregate is not in a state where it can be saved.
        /// </summary>
        private void ThrowExceptionIfCanNotSave()
        {
            if (!this.UserHasPermissionToSave())
                throw new Exception("User does not have permission to save.");

            else if (!this.Root.IsValid())
                throw new Exception("Unable to save at this time - data is not valid");
        }

        #endregion

        #region SaveAsync

        /// <summary>
        /// Instructs this aggregate to asynchronously save changes to its object graph back to 
        /// its data source.
        /// </summary>
        public Task SaveAsync()
        {
            // TODO: verify user has permission to save
            this.ThrowExceptionIfCanNotSave();

            return Task.Factory.StartNew(() =>
            {
                this.SynchronizeDtos();
                var ThisServiceProxy = DomainDataServiceClient.Create();
                this.DtoHost = ThisServiceProxy.Save(this.GetType().FullName, this.DtoHost, this.User.UserName, this.User.Password);
                this.BuildObjectGraph();
            });
        }

        #endregion

        #region SynchronizeDtos

        /// <summary>
        /// Synchronizes the DTOs with any data changes made to the object graph.
        /// </summary>
        protected abstract void SynchronizeDtos();

        #endregion

        #region INotifyPropertyChanged

        protected void RaisePropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null) this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region CreateAggregate
        
        /// <summary>
        /// Creates and returns a new instance of the identified aggregate type.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Never)]
        public static Aggregate CreateAggregate(string aggregateType)
        {
            // Create the AggregateType
            System.Type AggregateType;
            try
            {
                AggregateType = System.Type.GetType(aggregateType, true);
            }
            catch (Exception ex)
            {
                throw new Exception("Unable to get type of aggregate type: " + aggregateType, ex);
            }

            // Instantiate the AggregateType.
            object AggregateTypeInstance;
            try
            {
                AggregateTypeInstance = System.Activator.CreateInstance(AggregateType);
            }
            catch (Exception ex)
            {
                throw new Exception("Unable to create instance of aggregate type: " + aggregateType, ex);
            }

            // Verify that the type instantiated is an Aggregate.
            if (AggregateTypeInstance is Aggregate)
                return AggregateTypeInstance as Aggregate;
            else
                throw new Exception("Type does not derive from Aggregate.  Actual Type: " + AggregateTypeInstance.GetType().Name);

        }

        #endregion

    }

    #endregion

    #region IAggregateRoot

    /// <summary>
    /// An interface into an object that can be the Root of an Aggregate.
    /// </summary>
    public interface IAggregateRoot
    {
        /// <summary>
        /// Determines if this aggregate root is currently valid.
        /// </summary>
        bool IsValid();

        /// <summary>
        /// Gets a collection of rules that are currently invalid for the entities in this aggregate
        /// </summary>
        /// <returns></returns>
        RuleCollection GetInvalidRules();

    }

    #endregion

    #region FilterIsNullException

    /// <summary>
    /// An exception that indicates that an IFilter object was null.
    /// </summary>
    public class FilterIsNullException : Exception
    {
        public FilterIsNullException() { }
    }

    #endregion

    #region InvalidFilterException

    /// <summary>
    /// An exception that indicates that an IFilter object was used but is not valid for the
    /// current use.
    /// </summary>
    public class InvalidFilterException : Exception
    {

        public InvalidFilterException(string expectedFilterType, string receivedFilterType)
        {
            this.ExpectedFilterType = expectedFilterType;
            this.ReceivedFilterType = receivedFilterType;
        }

        public string ExpectedFilterType { get; private set; }
        public string ReceivedFilterType { get; private set; }

    }

    #endregion

    #region GetDtoHostException

    /// <summary>
    /// An exception that indicates that a GetDtoHost method failed.
    /// </summary>
    public class GetDtoHostException : Exception
    {

        public GetDtoHostException(string aggregateType, string filter, string message)
            : this(aggregateType, filter, message, null) { }

        public GetDtoHostException(string aggregateType, string filter, string message, Exception innnerException)
            : base(message, innnerException)
        {
            this.AggregateType = aggregateType;
            this.Filter = filter;
        }

        public string AggregateType { get; private set; }
        public string Filter { get; private set; }

    }

    #endregion

    #region SaveException

    /// <summary>
    /// An exception that indicates that a save operation failed.
    /// </summary>
    public class SaveException : Exception
    {

        public SaveException()
            : base("Unable to save at this time.  Make sure all business rules are satisfied and/or the identified user account has the proper permissions.") { }

        public SaveException(string message)
            : base(message) { }

    }

    #endregion

    #region SavingEventHandler

    public delegate void SavingEventHandler(object sender, SavingEventArgs e);

    /// <summary>
    /// Provides event data for when an aggregate is about to be saved.
    /// </summary>
    /// <remarks>
    /// The respective save operation can be cancelled by calling the Cancel method.
    /// </remarks>
    public class SavingEventArgs : EventArgs
    {
        public SavingEventArgs() { this.IsCancelled = false; }

        /// <summary>
        /// Identifies that the save operation should be cancelled.
        /// </summary>
        public bool IsCancelled { get; private set; }

        /// <summary>
        /// Identifies why this save operation was cancelled.
        /// </summary>
        public string ReasonForCancelling { get; private set; }

        /// <summary>
        /// Identifies that the save operation should be cancelled and the reason why.
        /// </summary>
        public void Cancel(string reasonForCancelling)
        {
            this.IsCancelled = true;
            this.ReasonForCancelling = reasonForCancelling;
        }

    }

    #endregion

    #region SaveCancelledException

    /// <summary>
    /// An exception that indicates that an Save or SaveAsync operation has been cancelled 
    /// for some reason.
    /// </summary>
    public class SaveCancelledException : Exception
    {

        internal SaveCancelledException(string reason)
            : base("Save Cancelled: " + reason)
        {
        }

    }

    #endregion

    #region SaveDtoException

    /// <summary>
    /// An exception that indicates that an SaveDto operation has failed.
    /// </summary>
    public class SaveDtoException : Exception
    {

        public SaveDtoException(Exception innerException) :
            base("There was an unexpected problem trying to save a DTO.", innerException)
        {

        }

    }

    #endregion

    #region DeleteDtoException

    /// <summary>
    /// An exception that indicates that an DeleteDto operation has failed.
    /// </summary>
    public class DeleteDtoException : Exception
    {

        public DeleteDtoException(Exception innerException) :
            base("There was an unexpected problem trying to delete a DTO.", innerException)
        {

        }

    }

    #endregion

}